7. 递归的数据类型

递归与递归数据类型在编程中十分重要,许多常见的数据结构都是递归定义的。

递归数据结构的定义包括两部分:

类似的,定义在这些递归数据类型上的方法也是递归定义的:你需要定义该方法在所有基本情形上的值,然后定义在构造情形中,如何基于已知的方法的值得到新的值。

模棱两可的构造定义

当数据类型的递归定义允许通过多种方式得到同一个元素时,我们称这种定义方法是 模棱两可 的。实际定义递归数据结构类型时要尽可能避免模棱两可的定义,因为这极有可能导致在该数据类型上定义的递归方法存在歧义。见下面这个例子:

我们尝试以如下递归方法定义 2 的所有幂的集合 P

  • 基本情形:1,2P
  • 构造情形:若 m,nP,则 m×nP

我们再定义如下方法 f

  • 基本情形:f(1)=0,f(2)=1
  • 构造情形:若 m,nP,则 f(m×n)=m+f(n)

这个定义乍看之下没有问题,但很容易就能发现 P 中的大量元素可以通过多种方式构造出来,而由于 f(mn)f(nm),导致了定义的该方法存在歧义。例如 f(16)=f(2×8)=5 同时 f(16)=f(4×4)=7

然而并非所有定义在模棱两可的递归类型上的方法都存在歧义。例如将上面的方法的构造定义修改为类似取对数的运算 f(m×n)=f(m)+f(n) 就不会引发歧义。

定义递归方法或递归函数时需要十分小心!!!

首先,当我们定义的递归方法没有遵循数据类型的递归定义时,常常会发生问题。我们以在 N 上定义的一些错误方法为例(N 的递归定义见下面):

  • 没有基本情形:
f(n)=c+f(n1)
  • 有基本情形,但递归无法终止:
f(n)={0,n=0f(n+1),otherwise
  • 定义矛盾
f(n)={0,2n1,3n2,otherwise

为了证明递归数据结构具有某些性质,可以使用 结构归纳法。结构归纳法与上面递归方法的定义过程与前面提到的 状态机上的归纳法 十分相像。其基本过程为:

P 是谓词,R 是某个递归数据类型的集合,BR 是所有的基本情形,c 是由已知元素构造出新元素的构造方法。则若

eiR.iP(ei)P(c(e1,e2,,en))

则对于任意 eRP(e) 成立。

我们可以使用递归来定义非负整数集 N

如果我们在该非负整数集的定义上应用结构归纳法,会发现它和前面讲过的 一般归纳法 完全一致,这表明,一般归纳法是结构归纳法的特例。

然而需要注意的是,有些递归方法会使用较大的参数对应的函数值来定义较小的参数对应的函数值,这些递归方法无法使用结构归纳法来判断其性质,这些方法的递归可能无法终止,但并不绝对。例如这两个定义在 N 上的经典的递归函数:

f(n)={1,n1f(n2),n>1,2nf(3n+1),n>1,2n

关于该函数一个著名猜想是 nN.f(n)=1,但至今仍未被证明或证伪。

A(m,n)={2n,m=0n1A(m1,A(m,n1)),otherwise

阿克曼函数增长极快,对应的,其反函数 α(x) 增长极慢。对于在实际过程中有意义的数据规模 xα(x)5α(x) 在计算并查集的时间复杂度时会用到,从实用角度而言可以看作常数。